/*
 * $Id: UserService.java 45 2013-05-03 20:32:47Z gabakyan $
 * $Author: gabakyan $
 * $Revision: 45 $
 * $Date: 2013-05-03 20:32:47 +0000 (Fri, 03 May 2013) $
 *
 * Copyright (c) 2013 Supply Chain Intelligence (SCI), Inc.
 * http://www.scintelligence.com/, Email: info@scintelligence.com
 * All rights reserved.
 *
 * This file is part of Logistics Map.
 *
 * Logistics Map is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, version 3 of the License.
 *
 * Logistics Map is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with Logistics Map.  If not, see <http://www.gnu.org/licenses/>.
 */

package com.sci.logisticsmap.service;

import com.sci.logisticsmap.data.model.UserModel;
import com.sci.logisticsmap.data.repository.UserRepository;
import com.sci.logisticsmap.data.support.sorting.UserModelSorting;
import com.sci.logisticsmap.service.support.*;
import com.sci.logisticsmap.service.support.builder.ProfileDTCommandBuilder;
import com.sci.logisticsmap.service.model.*;
import com.sci.logisticsmap.service.support.exception.userservice.UsernameExistsException;
import com.sci.logisticsmap.support.Constants;
import com.sci.logisticsmap.support.Page;
import com.sci.logisticsmap.support.SecurityRoles;
import com.sci.logisticsmap.support.Utilities;
import com.sci.logisticsmap.support.model.UserConfirmationType;
import com.sci.logisticsmap.support.model.UserStatus;
import com.sci.logisticsmap.support.model.UserType;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.mail.SimpleMailMessage;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

/**
 * The Service for working with User model.
 *
 * @author gabakyan@gmail.com (Last modified by $Author: gabakyan $)
 * @version $Revision: 45 $
 */
@Service
@Transactional
public class UserService extends BaseService {
    /**
     * The user repository.
     */
    @Autowired
    private UserRepository userRepository;

    /**
     * The user log service.
     */
    @Autowired
    private UserLogService userLogService;

    /**
     * The mail sender helper.
     */
    @Autowired
    private MailSenderHelper mailSenderHelper;

    /**
     * The simple code generator.
     */
    @Autowired
    private SimpleCodeGenerator codeGenerator;

    /**
     * The sign up confirmation mail message.
     */
    @Autowired
    private SimpleMailMessage signUpConfirmationMailMessage;
    /**
     * The password recovery mail message.
     */
    @Autowired
    private SimpleMailMessage passwordRecoveryMailMessage;

    /**
     * The sign up confirmation mail template location.
     */
    private @Value("${mail.templates.signUpConfirmationMailTemplateLocation}") String signUpConfirmationMailTemplateLocation;
    /**
     * The password recovery mail template location.
     */
    private @Value("${mail.templates.passwordRecoveryMailTemplateLocation}") String passwordRecoveryMailTemplateLocation;

    /**
     * The app url property.
     */
    private @Value("${app.url}") String appUrl;
    /**
     * The sponsored by url property.
     */
    private @Value("${app.sponsoredByUrl}") String sponsoredByUrl;

    /**
     * The authentication manager.
     */
    @Autowired
    @Qualifier("authenticationManager")
    private AuthenticationManager authenticationManager;

    /**
     * The sign up confirmation path.
     */
    public static final String PATH_SIGN_UP_CONFIRMATION = "/sign-up-confirmation";

    /**
     * Adds the user.
     *
     * @param command The user command.
     */
    public void add(UserDTCommand command) {
        try {
            UserModel user = createUser();
            translationUtilities.translate(command, user);

            userRepository.save(user);
            logger.debug("User saved successfully");
        } catch (Exception e) {
            throw new ServiceException(e);
        }
    }

    /**
     * Edits the user.
     *
     * @param command The user command.
     */
    public void edit(UserDTCommand command) {
        try {
            UserModel user = userRepository.findOne(command.getUsername());
            translationUtilities.translate(command, user);

            userRepository.save(user);
            GlobalBriefcase.INSTANCE.putUserStatus(user.getUsername(), user.getStatus());
            logger.debug("User saved successfully");
        } catch (Exception e) {
            throw new ServiceException(e);
        }
    }

    /**
     * Edits the profile.
     *
     * @param sessionUser The session user.
     * @param command The profile command.
     */
    public void editProfile(SessionUserDTModel sessionUser, ProfileDTCommand command) {
        try {
            UserModel user = userRepository.findOne(sessionUser.getUsername());
            translationUtilities.translate(command, user);

            userRepository.save(user);
            logger.debug("User saved successfully");

            ServiceUtilities.updateSessionUser(sessionUser, user);
        } catch (Exception e) {
            throw new ServiceException(e);
        }
    }

    /**
     * Deletes the user.
     *
     * @param username The username.
     *
     * @return <code>true</code> if the user was deleted, otherwise
     * <code>false</code>.
     */
    public boolean delete(String username) {
        try {
            UserModel user = userRepository.findOne(username);
            if (user != null) {
                userRepository.delete(user);
                logger.debug("User deleted successfully");

                return true;
            }
        } catch (Exception e) {
            throw new ServiceException(e);
        }
        return false;
    }

    /**
     * Retrieves the user model.
     *
     * @param username The username.
     *
     * @return The user model.
     */
    public UserDTModel getDTModel(String username) {
        try {
            UserModel user = userRepository.findOne(username);
            if (user != null) {
                return conversionUtilities.convert(user, UserDTModel.class);
            }
            return null;
        } catch (Exception e) {
            throw new ServiceException(e);
        }
    }

    /**
     * Retrieves the session user model by the username.
     *
     * @param username The username.
     *
     * @return The session user model.
     */
    public SessionUserDTModel getSessionUserDTModelByUsername(String username) {
        try {
            UserModel user = userRepository.findOne(username);
            if (user != null) {
                return conversionUtilities.convert(user, SessionUserDTModel.class);
            }
            return null;
        } catch (Exception e) {
            throw new ServiceException(e);
        }
    }

    /**
     * Retrieves the user models.
     *
     * @param page The page.
     *
     * @return The user models.
     */
    public Page<UserDTModel> getDTModels(int page) {
        return getDTModels(page, Constants.DEFAULT_PAGE_SIZE, null);
    }

    /**
     * Retrieves the user models.
     *
     * @param page The page.
     * @param pageSize The page size.
     *
     * @return The user models.
     */
    public Page<UserDTModel> getDTModels(int page, int pageSize) {
        return getDTModels(page, pageSize, null);
    }

    /**
     * Retrieves the user models.
     *
     * @param page The page.
     * @param keyword The keyword.
     *
     * @return The user models.
     */
    public Page<UserDTModel> getDTModels(int page, String keyword) {
        return getDTModels(page, Constants.DEFAULT_PAGE_SIZE, null);
    }

    /**
     * Retrieves the user models.
     *
     * @param page The page.
     * @param pageSize The page size.
     * @param keyword The keyword.
     *
     * @return The user models.
     */
    public Page<UserDTModel> getDTModels(int page, int pageSize, String keyword) {
        try {
            Page<UserModel> userPage = userRepository.findAll(page, pageSize, UserModelSorting.BY_USERNAME);
            if (userPage != null) {
                return conversionUtilities.convert(userPage, UserDTModel.class);
            }
            return null;
        } catch (Exception e) {
            throw new ServiceException(e);
        }
    }

    /**
     * Retrieves the user command.
     *
     * @param username The username.
     *
     * @return The user command.
     */
    public UserDTCommand getDTCommand(String username) {
        try {
            UserModel user = userRepository.findOne(username);
            if (user != null) {
                return conversionUtilities.convert(user, UserDTCommand.class);
            }
            return null;
        } catch (Exception e) {
            throw new ServiceException(e);
        }
    }

    /**
     * Retrieves the profile command.
     *
     * @param username The username.
     *
     * @return The profile command.
     */
    public ProfileDTCommand getProfileDTCommand(String username) {
        try {
            UserModel user = userRepository.findOne(username);
            if (user != null) {
                ProfileDTCommandBuilder builder = new ProfileDTCommandBuilder();
                builder.setUser(user);
                return builder.build();
            }
            return null;
        } catch (Exception e) {
            throw new ServiceException(e);
        }
    }

    /**
     * Signs up the user.
     *
     * @param command The sign up command.
     *
     * @return The session user model.
     */
    public SessionUserDTModel signUp(SignUpDTCommand command) {
        try {
            UserModel user = userRepository.findOne(command.getUsername());
            if (user == null) {
                /*User */user = createUser();
                translationUtilities.translate(command, user);

                user.setType(UserType.USER);
                user.setStatus(UserStatus.NEW_INCOMPLETE);

                List<String> roles = new ArrayList<String>();
                roles.add(SecurityRoles.ROLE_USER);
                user.setRoles(roles);

                userRepository.save(user);
                logger.debug("User saved successfully");

                authenticateUser(command.getUsername(), command.getPassword());
            } else {
                throw new UsernameExistsException();
            }

            SessionUserDTModel sessionUser = conversionUtilities.convert(user, SessionUserDTModel.class);

            userLogService.addSignUp(sessionUser);

            return sessionUser;
        } catch (ServiceException e) {
            throw e;
        } catch (Exception e) {
            throw new ServiceException(e);
        }
    }

    /**
     * Signs up the OpenID user.
     *
     * @param command The OpenID sign up command.
     *
     * @return The session user model.
     */
    public SessionUserDTModel openIDSignUp(OpenIDSignUpDTCommand command) {
        try {
            UserModel user = userRepository.findOne(command.getUsername());
            if (user == null) {
                /*User */user = createUser();
                translationUtilities.translate(command, user);

                user.setType(UserType.OPENID_USER);
                user.setStatus(UserStatus.VERIFIED_INCOMPLETE);

                List<String> roles = new ArrayList<String>();
                roles.add(SecurityRoles.ROLE_USER);
                user.setRoles(roles);

                userRepository.save(user);
                logger.debug("User saved successfully");
            }

            SessionUserDTModel sessionUser = conversionUtilities.convert(user, SessionUserDTModel.class);

            userLogService.addSignUp(sessionUser);

            return sessionUser;
        } catch (Exception e) {
            throw new ServiceException(e);
        }
    }

    /**
     * Continues the user sign up.
     *
     * @param sessionUser The session user.
     * @param command The sign up continue command.
     */
    public void signUpContinue(SessionUserDTModel sessionUser, SignUpContinueDTCommand command) {
        try {
            UserModel user = userRepository.findOne(sessionUser.getUsername());
            translationUtilities.translate(command, user);

            String confirmationCode = null;
            boolean sendConfirmation = false;

            if (user.getType() != null) {
                if (user.getType().equals(UserType.OPENID_USER)) {
                    user.setStatus(UserStatus.VERIFIED);
                } else {
                     if (user.getStatus().equals(UserStatus.NEW_INCOMPLETE)) {
                        user.setStatus(UserStatus.NEW);

                         List<UserModel.Confirmation> confirmations;
                         if ((confirmations = user.getConfirmations()) == null) {
                             confirmations = new ArrayList<UserModel.Confirmation>();
                             user.setConfirmations(confirmations);
                         }
                         confirmationCode = codeGenerator.nextString();
                         confirmations.add(user.new Confirmation(UserConfirmationType.SIGN_UP, confirmationCode));
                         sendConfirmation = true;
                     } else if (user.getStatus().equals(UserStatus.VERIFIED_INCOMPLETE)) {
                         user.setStatus(UserStatus.VERIFIED);
                     }
                }
            }

            userRepository.save(user);
            logger.debug("User saved successfully");

            if (sendConfirmation) {
                sendSignUpConfirmationMail(sessionUser, confirmationCode);
            }

            ServiceUtilities.updateSessionUser(sessionUser, user);

            //sessionUser = conversionUtilities.convert(user, SessionUserDTModel.class);
            //SecurityContextHelper.setAuthenticatedUserDTModel(sessionUser);
        } catch (Exception e) {
            throw new ServiceException(e);
        }
    }

    /**
     * Processes the sign up confirmation.
     *
     * @param sessionUser The session user.
     * @param username The username.
     * @param confirmationCode The confirmation code.
     *
     * @return <code>true</code> if the confirmation code is correct, otherwise
     * <code>false</code>.
     */
    public boolean signUpConfirmation(SessionUserDTModel sessionUser, String username, String confirmationCode) {
        try {
            UserModel user = userRepository.findOne(username);
            logger.debug("found user: username = {}", username);

            if (user != null) {
                for (UserModel.Confirmation confirmation : user.getConfirmations()) {
                    logger.debug("found confirmation: type = {}, code = {}, used = {}", confirmation.getType(), confirmation.getCode(), String.valueOf(confirmation.isUsed()));
                    if (confirmation.getType().equals(UserConfirmationType.SIGN_UP) && !confirmation.isUsed()) {
                        logger.debug("checking confirmation code: {} / {}", confirmation.getCode(), confirmationCode);
                        if (confirmation.getCode().equals(confirmationCode)) {
                            logger.debug("confirmation code found");
                            user.setStatus(UserStatus.VERIFIED);
                            confirmation.setUsed(true);
                            userRepository.save(user);

                            if (sessionUser != null) {
                                ServiceUtilities.updateSessionUser(sessionUser, user);

                                //sessionUser = conversionUtilities.convert(user, SessionUserDTModel.class);
                                //SecurityContextHelper.setAuthenticatedUserDTModel(sessionUser);
                            }
                            return true;
                        }
                    }
                }
            }
            return false;
        } catch (Exception e) {
            throw new ServiceException(e);
        }
    }

    /**
     * Authenticates the user.
     *
     * @param username The username.
     * @param password The password.
     *
     * @return <code>true</code> if the user was authenticated, otherwise
     * <code>false</code>.
     */
    private boolean authenticateUser(String username, String password) {
        boolean authenticated;
        try {
            Authentication authentication = authenticationManager.authenticate(new UsernamePasswordAuthenticationToken(username, password));
            SecurityContextHolder.getContext().setAuthentication(authentication);
            authenticated = authentication.isAuthenticated();
            logger.debug("User '{}' successfully logged in", username);
        /*} catch (BadCredentialsException e) {
            authenticated = false;*/
        } catch (AuthenticationException e) {
            logger.debug("User '{}' failed to login. Reason: {}", username, e.getMessage());
            authenticated = false;
        }
        return authenticated;
    }

    /**
     * Sends the sign up confirmation mail.
     *
     * @param sessionUser The session user.
     * @param confirmationCode The confirmation code.
     *
     * @return <code>true</code> if the sign up confirmation mail was sent, otherwise
     * <code>false</code>.
     */
    private boolean sendSignUpConfirmationMail(SessionUserDTModel sessionUser, String confirmationCode) throws Exception {
        StringBuilder stringBuilder = new StringBuilder();

        Map<String, String> messageTemplateModel = new HashMap<String, String>();

        addCommonMessagesToMessageTemplateModel(messageTemplateModel);

        messageTemplateModel.put("hi", messageSourceAccessHelper.getMessage("mail.signUpConfirmationMail.hi"));
        messageTemplateModel.put("useLinkBelow", messageSourceAccessHelper.getMessage("mail.signUpConfirmationMail.useLinkBelow"));

        stringBuilder.delete(0, stringBuilder.length());
        stringBuilder.append(sessionUser.getFirstName()).append(" ").append(sessionUser.getLastName());
        messageTemplateModel.put("username", stringBuilder.toString());

        stringBuilder.delete(0, stringBuilder.length());
        stringBuilder.append(appUrl).append(PATH_SIGN_UP_CONFIRMATION)
                .append("?email=").append(Utilities.encodeURLParameter(sessionUser.getUsername()))
                .append("&code=").append(Utilities.encodeURLParameter(confirmationCode));
        messageTemplateModel.put("confirmationUrl", stringBuilder.toString());

        messageTemplateModel.put("sponsoredByUrl", sponsoredByUrl);

        logger.debug("sendSignUpConfirmationMail: sending email...");
        logger.debug("messageTemplateModel = {}", messageTemplateModel);
        if (mailSenderHelper.sendMail(signUpConfirmationMailMessage, sessionUser.getUsername(), signUpConfirmationMailTemplateLocation, messageTemplateModel)) {
            return true;
        } else {
            return false;//throw new MessageKeyedException();
        }
    }

    /**
     * Adds the common messages to the message template model.
     *
     * @param messageTemplateModel The the message template model.
     */
    private void addCommonMessagesToMessageTemplateModel(Map<String, String> messageTemplateModel) {
        messageTemplateModel.put("commonPrivacyWarning", messageSourceAccessHelper.getMessage("mail.common.privacyWarning"));

        messageTemplateModel.put("commonSignatureThanks", messageSourceAccessHelper.getMessage("mail.common.signature.thanks"));
        messageTemplateModel.put("commonSignatureCopyright", messageSourceAccessHelper.getMessage("mail.common.signature.copyright"));
        messageTemplateModel.put("commonSignatureSponsoredBy", messageSourceAccessHelper.getMessage("mail.common.signature.sponsoredBy"));
        messageTemplateModel.put("commonSignatureCompanyName", messageSourceAccessHelper.getMessage("mail.common.signature.companyName"));
    }

    /**
     * Creates the user model.
     *
     * @return The user model.
     */
    private UserModel createUser() {
        UserModel user = new UserModel();
        user.setConfiguration(user.new Configuration());
        user.setConfirmations(new ArrayList<UserModel.Confirmation>());
        return user;
    }
}
